﻿using Microsoft.AspNetCore.Http;
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Tax;
using Nop.Plugin.Tax.FixedOrByCountryStateZip.Domain;
using Nop.Plugin.Tax.FixedOrByCountryStateZip.Infrastructure.Cache;
using Nop.Plugin.Tax.FixedOrByCountryStateZip.Services;
using Nop.Services.Common;
using Nop.Services.Configuration;
using Nop.Services.Localization;
using Nop.Services.Orders;
using Nop.Services.Payments;
using Nop.Services.Plugins;
using Nop.Services.Tax;

namespace Nop.Plugin.Tax.FixedOrByCountryStateZip;

/// <summary>
/// Fixed or by country & state & zip rate tax provider
/// </summary>
public class FixedOrByCountryStateZipTaxProvider : BasePlugin, ITaxProvider
{
    #region Fields

    protected readonly FixedOrByCountryStateZipTaxSettings _countryStateZipSettings;
    protected readonly ICountryStateZipService _taxRateService;
    protected readonly IGenericAttributeService _genericAttributeService;
    protected readonly IHttpContextAccessor _httpContextAccessor;
    protected readonly ILocalizationService _localizationService;
    protected readonly IOrderTotalCalculationService _orderTotalCalculationService;
    protected readonly IPaymentService _paymentService;
    protected readonly ISettingService _settingService;
    protected readonly IStaticCacheManager _staticCacheManager;
    protected readonly ITaxCategoryService _taxCategoryService;
    protected readonly ITaxService _taxService;
    protected readonly IWebHelper _webHelper;
    protected readonly TaxSettings _taxSettings;

    #endregion

    #region Ctor

    public FixedOrByCountryStateZipTaxProvider(FixedOrByCountryStateZipTaxSettings countryStateZipSettings,
        ICountryStateZipService taxRateService,
        IGenericAttributeService genericAttributeService,
        IHttpContextAccessor httpContextAccessor,
        ILocalizationService localizationService,
        IOrderTotalCalculationService orderTotalCalculationService,
        IPaymentService paymentService,
        ISettingService settingService,
        IStaticCacheManager staticCacheManager,
        ITaxCategoryService taxCategoryService,
        ITaxService taxService,
        IWebHelper webHelper,
        TaxSettings taxSettings)
    {
        _countryStateZipSettings = countryStateZipSettings;
        _taxRateService = taxRateService;
        _genericAttributeService = genericAttributeService;
        _httpContextAccessor = httpContextAccessor;
        _localizationService = localizationService;
        _orderTotalCalculationService = orderTotalCalculationService;
        _paymentService = paymentService;
        _settingService = settingService;
        _staticCacheManager = staticCacheManager;
        _taxCategoryService = taxCategoryService;
        _taxService = taxService;
        _webHelper = webHelper;
        _taxSettings = taxSettings;
    }

    #endregion

    #region Methods

    /// <summary>
    /// Gets tax rate
    /// </summary>
    /// <param name="taxRateRequest">Tax rate request</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ax
    /// </returns>
    public async Task<TaxRateResult> GetTaxRateAsync(TaxRateRequest taxRateRequest)
    {
        var result = new TaxRateResult();

        //the tax rate calculation by fixed rate
        if (!_countryStateZipSettings.CountryStateZipEnabled)
        {
            result.TaxRate = await _settingService.GetSettingByKeyAsync<decimal>(string.Format(FixedOrByCountryStateZipDefaults.FIXED_RATE_SETTINGS_KEY, taxRateRequest.TaxCategoryId));
            return result;
        }

        //the tax rate calculation by country & state & zip 
        if (taxRateRequest.Address == null)
        {
            result.Errors.Add("Address is not set");
            return result;
        }

        //first, load all tax rate records (cached) - loaded only once
        var cacheKey = _staticCacheManager.PrepareKeyForDefaultCache(ModelCacheEventConsumer.ALL_TAX_RATES_MODEL_KEY);
        var allTaxRates = await _staticCacheManager.GetAsync(cacheKey, async () => (await _taxRateService.GetAllTaxRatesAsync()).Select(taxRate => new TaxRate
        {
            Id = taxRate.Id,
            StoreId = taxRate.StoreId,
            TaxCategoryId = taxRate.TaxCategoryId,
            CountryId = taxRate.CountryId,
            StateProvinceId = taxRate.StateProvinceId,
            Zip = taxRate.Zip,
            Percentage = taxRate.Percentage
        }).ToList());

        var storeId = taxRateRequest.CurrentStoreId;
        var taxCategoryId = taxRateRequest.TaxCategoryId;
        var countryId = taxRateRequest.Address.CountryId;
        var stateProvinceId = taxRateRequest.Address.StateProvinceId;
        var zip = taxRateRequest.Address.ZipPostalCode?.Trim() ?? string.Empty;

        var existingRates = allTaxRates.Where(taxRate => taxRate.CountryId == countryId && taxRate.TaxCategoryId == taxCategoryId);

        //filter by store
        var matchedByStore = existingRates.Where(taxRate => storeId == taxRate.StoreId || taxRate.StoreId == 0);

        //filter by state/province
        var matchedByStateProvince = matchedByStore.Where(taxRate => stateProvinceId == taxRate.StateProvinceId || taxRate.StateProvinceId == 0);

        //filter by zip
        var matchedByZip = matchedByStateProvince.Where(taxRate => string.IsNullOrWhiteSpace(taxRate.Zip) || taxRate.Zip.Equals(zip, StringComparison.InvariantCultureIgnoreCase));

        //sort from particular to general, more particular cases will be the first
        var foundRecords = matchedByZip.OrderBy(r => r.StoreId == 0).ThenBy(r => r.StateProvinceId == 0).ThenBy(r => string.IsNullOrEmpty(r.Zip));

        var foundRecord = foundRecords.FirstOrDefault();

        if (foundRecord != null)
            result.TaxRate = foundRecord.Percentage;

        return result;
    }

    /// <summary>
    /// Gets tax total
    /// </summary>
    /// <param name="taxTotalRequest">Tax total request</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ax total
    /// </returns>
    public async Task<TaxTotalResult> GetTaxTotalAsync(TaxTotalRequest taxTotalRequest)
    {
        if (_httpContextAccessor.HttpContext.Items.TryGetValue("nop.TaxTotal", out var result)
            && result is (TaxTotalResult taxTotalResult, decimal paymentTax))
        {
            //short-circuit to avoid circular reference when calculating payment method additional fee during the checkout process
            if (!taxTotalRequest.UsePaymentMethodAdditionalFee)
                return new TaxTotalResult { TaxTotal = taxTotalResult.TaxTotal - paymentTax };

            return taxTotalResult;
        }

        var taxRates = new SortedDictionary<decimal, decimal>();
        var taxTotal = decimal.Zero;

        //order sub total (items + checkout attributes)
        var (_, _, _, _, orderSubTotalTaxRates) = await _orderTotalCalculationService
            .GetShoppingCartSubTotalAsync(taxTotalRequest.ShoppingCart, false);
        var subTotalTaxTotal = decimal.Zero;
        foreach (var kvp in orderSubTotalTaxRates)
        {
            var taxRate = kvp.Key;
            var taxValue = kvp.Value;
            subTotalTaxTotal += taxValue;

            if (taxRate > decimal.Zero && taxValue > decimal.Zero)
            {
                if (!taxRates.TryGetValue(taxRate, out var value))
                    taxRates.Add(taxRate, taxValue);
                else
                    taxRates[taxRate] = value + taxValue;
            }
        }
        taxTotal += subTotalTaxTotal;

        //shipping
        var shippingTax = decimal.Zero;
        if (_taxSettings.ShippingIsTaxable)
        {
            var (shippingExclTax, _, _) = await _orderTotalCalculationService
                .GetShoppingCartShippingTotalAsync(taxTotalRequest.ShoppingCart, false);
            var (shippingInclTax, taxRate, _) = await _orderTotalCalculationService
                .GetShoppingCartShippingTotalAsync(taxTotalRequest.ShoppingCart, true);
            if (shippingExclTax.HasValue && shippingInclTax.HasValue)
            {
                shippingTax = shippingInclTax.Value - shippingExclTax.Value;
                if (shippingTax < decimal.Zero)
                    shippingTax = decimal.Zero;

                if (taxRate > decimal.Zero && shippingTax > decimal.Zero)
                {
                    if (!taxRates.TryGetValue(taxRate, out var value))
                        taxRates.Add(taxRate, shippingTax);
                    else
                        taxRates[taxRate] = value + shippingTax;
                }
            }
        }
        taxTotal += shippingTax;

        //short-circuit to avoid circular reference when calculating payment method additional fee during the checkout process
        if (!taxTotalRequest.UsePaymentMethodAdditionalFee)
            return new TaxTotalResult { TaxTotal = taxTotal };

        //payment method additional fee
        var paymentMethodAdditionalFeeTax = decimal.Zero;
        if (_taxSettings.PaymentMethodAdditionalFeeIsTaxable)
        {
            var paymentMethodSystemName = taxTotalRequest.Customer != null
                ? await _genericAttributeService
                    .GetAttributeAsync<string>(taxTotalRequest.Customer, NopCustomerDefaults.SelectedPaymentMethodAttribute, taxTotalRequest.StoreId)
                : string.Empty;

            var paymentMethodAdditionalFee = await _paymentService
                .GetAdditionalHandlingFeeAsync(taxTotalRequest.ShoppingCart, paymentMethodSystemName);
            var (paymentMethodAdditionalFeeExclTax, _) = await _taxService
                .GetPaymentMethodAdditionalFeeAsync(paymentMethodAdditionalFee, false, taxTotalRequest.Customer);
            var (paymentMethodAdditionalFeeInclTax, taxRate) = await _taxService
                .GetPaymentMethodAdditionalFeeAsync(paymentMethodAdditionalFee, true, taxTotalRequest.Customer);

            paymentMethodAdditionalFeeTax = paymentMethodAdditionalFeeInclTax - paymentMethodAdditionalFeeExclTax;
            if (paymentMethodAdditionalFeeTax < decimal.Zero)
                paymentMethodAdditionalFeeTax = decimal.Zero;

            if (taxRate > decimal.Zero && paymentMethodAdditionalFeeTax > decimal.Zero)
            {
                if (!taxRates.TryGetValue(taxRate, out var value))
                    taxRates.Add(taxRate, paymentMethodAdditionalFeeTax);
                else
                    taxRates[taxRate] = value + paymentMethodAdditionalFeeTax;
            }
        }
        taxTotal += paymentMethodAdditionalFeeTax;

        //add at least one tax rate (0%)
        if (!taxRates.Any())
            taxRates.Add(decimal.Zero, decimal.Zero);

        if (taxTotal < decimal.Zero)
            taxTotal = decimal.Zero;

        taxTotalResult = new TaxTotalResult { TaxTotal = taxTotal, TaxRates = taxRates, };

        //store values within the scope of the request to avoid duplicate calculations
        _httpContextAccessor.HttpContext.Items.TryAdd("nop.TaxTotal", (taxTotalResult, paymentMethodAdditionalFeeTax));

        return taxTotalResult;
    }

    /// <summary>
    /// Gets a configuration page URL
    /// </summary>
    public override string GetConfigurationPageUrl()
    {
        return $"{_webHelper.GetStoreLocation()}Admin/FixedOrByCountryStateZip/Configure";
    }

    /// <summary>
    /// Install plugin
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public override async Task InstallAsync()
    {
        //settings
        await _settingService.SaveSettingAsync(new FixedOrByCountryStateZipTaxSettings());

        //locales
        await _localizationService.AddOrUpdateLocaleResourceAsync(new Dictionary<string, string>
        {
            ["Plugins.Tax.FixedOrByCountryStateZip.Fixed"] = "Fixed rate",
            ["Plugins.Tax.FixedOrByCountryStateZip.Tax.Categories.Manage"] = "Manage tax categories",
            ["Plugins.Tax.FixedOrByCountryStateZip.TaxCategoriesCanNotLoaded"] = "No tax categories can be loaded. You may manage tax categories by <a href='{0}'>this link</a>",
            ["Plugins.Tax.FixedOrByCountryStateZip.TaxByCountryStateZip"] = "By Country",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.TaxCategoryName"] = "Tax category",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.Rate"] = "Rate",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.Store"] = "Store",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.Store.Hint"] = "If an asterisk is selected, then this shipping rate will apply to all stores.",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.Country"] = "Country",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.Country.Hint"] = "The country.",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.StateProvince"] = "State / province",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.StateProvince.Hint"] = "If an asterisk is selected, then this tax rate will apply to all customers from the given country, regardless of the state.",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.Zip"] = "Zip",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.Zip.Hint"] = "Zip / postal code. If zip is empty, then this tax rate will apply to all customers from the given country or state, regardless of the zip code.",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.TaxCategory"] = "Tax category",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.TaxCategory.Hint"] = "The tax category.",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.Percentage"] = "Percentage",
            ["Plugins.Tax.FixedOrByCountryStateZip.Fields.Percentage.Hint"] = "The tax rate.",
            ["Plugins.Tax.FixedOrByCountryStateZip.AddRecord"] = "Add tax rate",
            ["Plugins.Tax.FixedOrByCountryStateZip.AddRecordTitle"] = "New tax rate",
            ["Plugins.Tax.FixedOrByCountryStateZip.SwitchRate"] = @"
                    <p>
                        You are going to change the way the tax rate is calculated. This will cause the tax rate to be calculated based on the settings specified on the configuration page.
                    </p>
                    <p>
                        Any current tax rate settings will be saved, but will not be active until you return to this tax calculation method.
                    </p>",
        });

        await base.InstallAsync();
    }

    /// <summary>
    /// Uninstall plugin
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public override async Task UninstallAsync()
    {
        //settings
        await _settingService.DeleteSettingAsync<FixedOrByCountryStateZipTaxSettings>();

        //fixed rates
        var fixedRates = await (await _taxCategoryService.GetAllTaxCategoriesAsync())
            .SelectAwait(async taxCategory => await _settingService.GetSettingAsync(string.Format(FixedOrByCountryStateZipDefaults.FIXED_RATE_SETTINGS_KEY, taxCategory.Id)))
            .Where(setting => setting != null).ToListAsync();
        await _settingService.DeleteSettingsAsync(fixedRates);

        //locales
        await _localizationService.DeleteLocaleResourcesAsync("Plugins.Tax.FixedOrByCountryStateZip");

        await base.UninstallAsync();
    }

    #endregion
}